package doc.ic.walkmore.services;

import java.io.FileOutputStream;
import java.io.IOException;

import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;

import doc.ic.walkmore.DAO.StepsDataSource;
import doc.ic.walkmore.DAO.StepsDataSourceWrittable;
import doc.ic.walkmore.toolbox.ToolBox;

public class DetectMvtSensorService extends Service implements SensorEventListener {

	private static final String LOGTAG = "DetectMvtSensorService";
	protected static final long SCREEN_OFF_RECEIVER_DELAY = 500;//Milliseconds to wait after screen off
	
	private long lastEventTimestamp = 0;	
	private int nbSamplesSaved = 0;

	private DescriptiveStatistics xFactory;
	private DescriptiveStatistics yFactory;
	private DescriptiveStatistics zFactory;

	private String filename;
	private FileOutputStream mFos;
	private boolean isSensing = false;
    
	// Sensing Service
	private boolean mSensorServiceBound;
	private ActivitySensorService mSensorService;
	private Intent mSensorServiceIntent;
	
	// Scheduler
	private final Handler handler=new Handler();

	// Datasource
	private StepsDataSourceWrittable datasrc;
	
	// Sensor manager service
	private SensorManager mSensorManager;
    
	// PowerManager and WaveLock
	private PowerManager pm;
	private PowerManager.WakeLock wl;

    // Binder given to clients
    private final IBinder mBinder = new LocalBinder();
    
    /**
     * Class used for the client Binder.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */
    public class LocalBinder extends Binder {
    	public DetectMvtSensorService getService() {
            // Return this instance of Service so clients can call public methods
            return DetectMvtSensorService.this;
        }
    }
    
    // My receiver, after 500ms of SCREEN OFF, try to unregister then re-register the sensor
    public BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.i(LOGTAG, "onReceive("+intent+")");

            if (!intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
                return;
            }
             
            Runnable runnable = new Runnable() {
                public void run() {
                    Log.i(LOGTAG, "Runnable executing.");
                    unregisterListener();
                    registerListener();
                }
            };

            if (isSensing)
            	new Handler().postDelayed(runnable, SCREEN_OFF_RECEIVER_DELAY);
        }
    };
    
	/** Defines callbacks for service binding, passed to bindService() */
    private ServiceConnection mSensorConnection = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // We've bound to LocalService, cast the IBinder and get LocalService instance
            ActivitySensorService.LocalBinder binder = (ActivitySensorService.LocalBinder) service;
            mSensorService = binder.getService();
            mSensorServiceBound = true;
            mSensorService.setDataSource(datasrc);
            // Start this service every 15sec
			launchDetectMvtScheduler();
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            mSensorServiceBound = false;
        }
    };
	
	// On creation of this service	
    @Override
    public void onCreate() {
		// ---------------------
		// Start sensing service
		// ---------------------
	   	// Intent for the activity sensoring service
	   	mSensorServiceIntent = new Intent(getApplicationContext(), ActivitySensorService.class);
	   	// Explicitly start the service, to leave it running (so explicitly have to stop it)
	   	startService(mSensorServiceIntent);
	   	// doBind service
	   	bindService(mSensorServiceIntent, mSensorConnection, Context.BIND_AUTO_CREATE);
	   	// ---------------------
	   	
	   	// Get powerManager
	   	pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
	   	wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "detectMvt");
	   	
	   	// Get SensorManager
		mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
	}
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(LOGTAG, "Received start id " + startId + ": " + intent);
        // We want this service to continue running until it is explicitly
        // stopped, so return sticky.
    	return START_STICKY;
    }
    
    private void registerListener() {
		// Register the sensor listener
	    mSensorManager.registerListener(this,
	        mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
	        SensorManager.SENSOR_DELAY_GAME);    	
    }
    
    private void unregisterListener() {
		// Unregister the listener
		mSensorManager.unregisterListener(this);
    }

	@Override
	public void onAccuracyChanged(Sensor arg0, int arg1) {
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
        synchronized (this) {
            if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            	// /!\ event.timestamp is in NANOseconds!
            	if (event.timestamp - lastEventTimestamp > ToolBox.SAMPLING_PERIOD_MS*1000000 && nbSamplesSaved<1000/ToolBox.SAMPLING_PERIOD_MS) {
				// What to do after every sampling period
	            	// Store data
                	String data = event.timestamp + "," + event.values[0] + "," + event.values[1] + "," + event.values[2]+"\n";
                	 Log.i(LOGTAG, data);
            		try {
    					mFos.write(data.getBytes());
    				} catch (IOException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
				    xFactory.addValue(event.values[0]);
					yFactory.addValue(event.values[1]);
					zFactory.addValue(event.values[2]);
					
                	nbSamplesSaved++;
        			// We have sensed for 2sec (NOT SURE ABOUT THIS), enough to detect movement or not
        			if (nbSamplesSaved >= 1000/ToolBox.SAMPLING_PERIOD_MS) {
        				finishDetectActivity();
        			}
        			lastEventTimestamp = event.timestamp;
                }
			}
        }
	}

	private void finishDetectActivity() {		
		synchronized (this) {
			double mean = xFactory.getN()>0 ?
				xFactory.getStandardDeviation() + yFactory.getStandardDeviation() + zFactory.getStandardDeviation() :
				0;
			// Calculate sum of mean of the three axis, 
			String mvt = "movement";
			
			// Stop this service
			stopSensing();
	
			if (mean/3.0 > ToolBox.MOVEMENT_THRESHOLD) 
				startActivitySensingWith(filename);
			else {
				mvt = "still";
				// Delete temporary file
				deleteFile(filename);
			}

			Log.i(LOGTAG, "detectMvt service : "+mvt);
		}
	}	
	
	@Override
	public IBinder onBind(Intent intent) {
		 return mBinder;
	}
    
    public void startSensing() {
    	if (!isSensing()) {
	    	// Create temp file
			try {
				filename = "temp-"+System.currentTimeMillis()+".dat";
	            mFos = openFileOutput(filename, Context.MODE_APPEND);
	            mFos.write(("timestamp,x,y,z\n".getBytes()));
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			// Initialise required fields
			nbSamplesSaved = 0;
			lastEventTimestamp = 0;
			xFactory = new DescriptiveStatistics();
			yFactory = new DescriptiveStatistics();
			zFactory = new DescriptiveStatistics();
	
			
	        // Register a receiver on SCREEN OFF event
	        registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
	        // Register this sensor listener
	    	unregisterListener();
	        registerListener();
	        // Aquire wave lock
	    	wl.acquire();
	    	
	    	Log.i(LOGTAG, "Service started sensing");
	    	
	    	isSensing = true;	
    	}
    }
    
    // Started by user button or by detectionMvt service
	public void startActivitySensing() {		
		if (!mSensorService.isSensing()) {						
			// Start sensing activity service
			mSensorService.startSensing();
		}
	}
	
	// Started by DetectMvt service
	private void startActivitySensingWith(String filename) {
		mSensorService.fillDataWindow(filename);
		startActivitySensing();
	}
	
	// Stop sensing activity
	public void stopActivitySensing() {
		if (mSensorService.isSensing()) {
			mSensorService.stopSensing();
		}
	}
	
	/**
	 * We wanted to set an alarm to call the service every XX seconds, but service can't be bind to alarms
	 * We need to find a workaround for this.
	 * Workaround : a scheduler 
	 */
    public void launchDetectMvtScheduler() {
		final Runnable senser = new Runnable() {
			@Override
			public void run() {
				Log.i(LOGTAG, "Scheduled thread running.");
				if (isSensing)
					stopSensing();
				if (mSensorServiceBound && !mSensorService.isSensing() && !isSensing)
					startSensing();	
					
				// Re-run in 15sec
				handler.postDelayed(this, 15000);
				// /!\ WE WILL HAVE TO STOP THIS INFINITE postDelayed /!\
			}
		};

		// Set the new scheduler to run the runnable in 15sec
		handler.postDelayed(senser, 15000);
    }
    
    
	public void stopSensing() {
		if (isSensing) {
			try {
				mFos.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			unregisterReceiver(mReceiver);
			unregisterListener();
	        // Release wave lock
	    	wl.release();
	    	
	    	
	    	Log.i(LOGTAG, "Service stopped sensing");
			
			isSensing = false;
		}
	}
	
	@Override
	public void onDestroy() {
		if(isSensing)
			stopSensing();
		
    	// Stop sensing service
    	if (mSensorServiceBound)
    		unbindService(mSensorConnection);
    	stopService(mSensorServiceIntent);
    	
		
        Log.i(LOGTAG, "service terminated");
        
        super.onDestroy();
	}

	public boolean isSensing() {
		return isSensing;
	}

	public void setDataSource(StepsDataSourceWrittable datasource) {
		datasrc = datasource;
	}
}

